[CS]聊聊字符编码


源起一个Bug

用爬虫在百度爬图片的时候,发现部分查询关键字的时候,出现爬不出图片的情况.比如在爬的时候,就没有结果.爬鱼 图片就会有结果.

经过异常捕获,发现,在对URL转码的时候出现了转码错误:

html = requests.get(url, timeout=10).content.decode('utf-8')

error:
    html = requests.get(url, timeout=10).content.decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe7 in position 63897: invalid continuation byte

Log的意思是,UTF-8的解码器无法处理字符0xe7

最后的解决方案是使用latin-1编码:

html = requests.get(url, timeout=10).content.decode('latin-1')

不过这里遗留了几个问题:

  • 0xe7是什么?
  • latin-1是什么编码?

下面就要聊一聊这些问题.

计算机编码

在计算机中,所有数据在存储和运算时都要使用二进制表示(这个不理解可以问问冯诺依曼),也就是说,所有字母,符号在计算机中都是由01组成的一串数字表示.但是,就像我们需要给所有事物起名一样,所有现实中的字母,符号都需要一个对应的0 1字串表示,即编码.为了方便大家编码互通,就需要制定统一的编码规则,ASCII码就这么产生了.

ASCII编码

学编程的时候,ASCII是最早介绍的字符编码.
标准ASCII使用7位二进制数,因为一个字节占8位,所以在第一位补0形成8位.

举个栗子:
ASCII编码中,字母A的表示为:

  • 二进制: 0100 0001
  • 十进制: 65
  • 十六进制: 0x41

所以看前面的问题:0xe7是什么?
0x是16进制,用二进制表示就是1110 0111,十进制是231.

对照ASCII码,7位一共128个字符,231明显超过了128,所以对于ASCII编码来说,它并不认识0xe7.
这也说明了一个问题,由于ASCII编码长度很短,可以表示的字符有限,遇到中文或者其他字符,就需要其他编码来表示.

比如中文,上万个汉子需要表示,仅用1个字节表示$2^8$个字符是不够的.所以像GB2312就是使用两字节表示一个汉字,一共$2^8$ * $2^8$ = 65536个

Unicode编码

因为存在不同的编码,所以打开文件前就需要指定正确的编码格式,不然解码出来的都是乱码.
那么能不能出一种编码,能够涵盖所有的字符呢?Unicode就是这么样的一个符号集.

但是Unicode只是一个符号集,只规定符号二进制代码,没有规定二进制如何存储.

举个栗子:
汉字,用Unicode表示为十六进制的9c7c:

>>> u'鱼'
u'\u9c7c'

用二进制表示为1001 1100 0111 1100,一共16位,所以至少需要两个字节表示它.

所以问题来了

  • 怎么确定它是Unicode而不是两个字符组成的ASCII?
  • 为了解决上面的问题,如果所有字符都用两字节表示,那么只用到7位的字符A就会浪费掉将近一个字节的空间.这怎么解决?

UTF-8

为了解决空间浪费的问题,出现了一些中间格式的字符集,他们被称为通用转换格式,即UTF(Unicode Transformation Format)。常见的UTF格式有:UTF-7, UTF-7.5, UTF-8,UTF-16, 以及 UTF-32。

主要聊聊常见的UTF-8
UTF-8规则:

  • 如果字符只有一个字节则其最高二进制位为0,后7位是字符的Unicode码.单字节的编码和ASCII一致
  • 对于N字节(N>1),第一个字节前N位设为1,第N+1位为0,其余各字节均以10开头
    2字节: 110xxxxx 10xxxxxx
    3字节: 1110xxxx 10xxxxxx 10xxxxxx
    4字节: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    

试着把前面的字转成UTF-8,的二进制16位,正好可以填入3字节的UTF-8:11101001 10110001 10111100,将其转成16进制就是e9b1bc.

填入顺序是从低字节开始填,到高字节填不满的补0

用代码验证一下:

>>> u'鱼'.encode('utf-8')
'\xe9\xb1\xbc'

了解了UTF-8,再回看之前的Bug:0xe7的二进制是1110 0111,占2字节,对比UTF-8的2字节,第三位就不一样,所以0xe7不属于UTF-8的格式,因此无法解码.

ISO/IEC 8859-1

看到这个编码名字是否是一脸懵逼?不过提起别称就清楚了,它就是Latin-1编码.

Latin-1属于单字节编码,最多能表示0-255的范围,即$2^8$,所以0xe7就在它的表示范围内,因此可以解码.

单字节编码的问题是能够表示的字符很少,但是单字节和计算机最基础的表示单位一致,所以面对其他编码的中文表示,可以拆成一个一个的单字节,用Latin-1进行保存.所以就像上面UTF-8的表示一样,用Latin-1解码后,单个字节拼起来就是UTF-8编码了:

>>> b'鱼'.decode('Latin-1')
u'\xe9\xb1\xbc'

以上,问题解决.


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录